package backend

import (
	"bytes"
	"encoding/json"
	"html/template"
	"lanmon/internal/config"
	"lanmon/internal/utils"
	"sync"
	"time"

	paho "github.com/eclipse/paho.mqtt.golang"
	"github.com/pkg/errors"
	log "github.com/sirupsen/logrus"
)

// MQTT lanmon-server
type LanClient struct {
	info *utils.SysInfo
}

type MqttServer struct {
	sync.RWMutex

	conn                 paho.Client
	eventTopic           string
	broadcastTopic       string
	commandTopicTemplate *template.Template
	qos                  uint8
	server               string

	Clients map[string]*LanClient
}

func setupMqttServer(c *config.Config) (*MqttServer, error) {
	conf := &c.MQTT

	var err error
	b := &MqttServer{
		eventTopic:     conf.EventTopic,
		broadcastTopic: conf.BroadcastTopic,
		qos:            conf.QOS,
		server:         conf.Server,
		Clients:        make(map[string]*LanClient),
	}

	b.commandTopicTemplate, err = template.New("command").Parse(conf.CommandTopicTemplate)
	if err != nil {
		return nil, errors.Wrap(err, "parse command topic template error")
	}

	broadcastDied := newMqttBroadcast(BroadcastDied, nil)
	diedPayload, err := broadcastDied.toJson()
	if err != nil {
		return nil, errors.Wrap(err, "parse died broadcast error")
	}

	opts := paho.NewClientOptions()
	opts.AddBroker(conf.Server)
	opts.SetUsername(conf.Username)
	opts.SetPassword(conf.Password)
	opts.SetCleanSession(conf.CleanSession)
	opts.SetClientID(conf.ClientID)
	opts.SetOnConnectHandler(b.onConnected)
	opts.SetConnectionLostHandler(b.onConnectionLost)
	opts.SetMaxReconnectInterval(conf.MaxReconnectInterval)
	opts.SetWill(b.broadcastTopic, diedPayload, b.qos, false)

	log.WithField("server", conf.Server).Info("connecting to mqtt broker")
	b.conn = paho.NewClient(opts)
	for {
		if token := b.conn.Connect(); token.Wait() && token.Error() != nil {
			log.Errorf("connecting to mqtt broker failed, will retry in 2s: %s", token.Error())
			time.Sleep(2 * time.Second)
		} else {
			break
		}
	}

	return b, nil
}

func (b *MqttServer) onConnected(c paho.Client) {
	log.Info("connected to mqtt server")
	b.connect()
}

func (b *MqttServer) onConnectionLost(c paho.Client, reason error) {
	b.died()
	log.Errorf("mqtt connection error: %s", reason)
}

func (b *MqttServer) connect() error {
	for {
		log.WithFields(log.Fields{
			"topic": b.eventTopic,
			"qos":   b.qos,
		}).Info("subscribing to gateway event topic")
		if token := b.conn.Subscribe(b.eventTopic, b.qos, b.eventHandler); token.Wait() && token.Error() != nil {
			log.WithError(token.Error()).WithFields(log.Fields{
				"topic": b.eventTopic,
				"qos":   b.qos,
			}).Errorf("subscribe error")
			time.Sleep(time.Second)
			continue
		}
		break
	}

	return b.publishBroadcast(newMqttBroadcast(BroadcastClientLogin, nil))
}

func (b *MqttServer) eventHandler(c paho.Client, msg paho.Message) {
	payload := string(msg.Payload())
	log.WithFields(log.Fields{
		"topic":   msg.Topic(),
		"payload": payload,
	}).Debug("handle event")
	b.dispatchEvent(payload)
}

func (b *MqttServer) dispatchEvent(payload string) error {
	var event MqttEvent
	if err := event.fromJson(payload); err != nil {
		return err
	}

	// TODO:
	switch event.Action {
	case EventLogin:
		return b.clientLogin(&event)
	case EventLogout:
		return b.clientLogout(&event)
	}
	return nil
}

func (b *MqttServer) publishBroadcast(broadcast *MqttBroadcast) error {
	payload, err := broadcast.toJson()
	if err != nil {
		return errors.Wrap(err, "convert broadcast error")
	}

	if token := b.conn.Publish(b.broadcastTopic, b.qos, false, []byte(payload)); token.Wait() && token.Error() != nil {
		return errors.Wrap(token.Error(), "publish gateway broadcast error")
	}
	return nil
}

func (b *MqttServer) publishCommand(command *MqttCommand) error {
	payload, err := command.toJson()
	if err != nil {
		return err
	}

	templateCtx := struct {
		ClientId string
	}{command.ClientId}
	topic := bytes.NewBuffer(nil)
	if err := b.commandTopicTemplate.Execute(topic, templateCtx); err != nil {
		return errors.Wrap(err, "execute command topic template error")
	}

	if token := b.conn.Publish(topic.String(), b.qos, false, []byte(payload)); token.Wait() && token.Error() != nil {
		return errors.Wrap(token.Error(), "publish command error")
	}

	return nil
}

func (b *MqttServer) died() error {
	b.clientCleanup()
	return b.publishBroadcast(newMqttBroadcast(BroadcastDied, nil))
}

func (b *MqttServer) clientCleanup() error {
	b.Lock()
	defer b.Unlock()

	b.Clients = make(map[string]*LanClient)
	return nil
}

//
// client handler
func (b *MqttServer) clientLogin(event *MqttEvent) error {
	log.WithFields(log.Fields{
		"clientId": event.ClientId,
	}).Info("client login")

	sysinfo := event.Args["sysinfo"]
	sysinfoBytes, err := json.Marshal(sysinfo)
	if err != nil {
		return err
	}

	var si utils.SysInfo
	err = si.FromJsonBytes(sysinfoBytes)
	if err != nil {
		return err
	}

	b.Lock()
	defer b.Unlock()
	b.Clients[event.ClientId] = &LanClient{info: &si}

	return nil
}

func (b *MqttServer) clientLogout(event *MqttEvent) error {
	log.WithFields(log.Fields{
		"clientId": event.ClientId,
	}).Info("client logout")

	b.Lock()
	defer b.Unlock()
	_, exist := b.Clients[event.ClientId]
	if exist {
		delete(b.Clients, event.ClientId)
	}

	return nil
}

func (b *MqttServer) HasClient(clientId string) bool {
	b.Lock()
	defer b.Unlock()
	for key, _ := range b.Clients {
		if key == clientId {
			return true
		}
	}
	return false
}

func (b *MqttServer) BroadcastShutdown(reboot bool) error {
	log.WithFields(log.Fields{
		"reboot": reboot,
	}).Info("broadcast shutdown")

	var args map[string]interface{}
	if reboot {
		args = make(map[string]interface{})
		args["reboot"] = 1
	}
	return b.publishBroadcast(newMqttBroadcast(BroadcastShutdown, args))
}

func (b *MqttServer) CommandShutdown(clientId string, reboot bool) error {
	log.WithFields(log.Fields{
		"reboot": reboot,
	}).Info("command shutdown")

	var args map[string]interface{}
	if reboot {
		args = make(map[string]interface{})
		args["reboot"] = 1
	}
	return b.publishCommand(newMqttCommand(clientId, CommandShutdown, args))
}
